//
// Copyright 2023 Google LLC
// SPDX-License-Identifier: Apache-2.0
//

// go/keep-sorted start
@use 'sass:list';
@use 'sass:map';
// go/keep-sorted end
// go/keep-sorted start
@use '../../elevation/elevation';
@use '../../ripple/ripple';
@use '../../tokens';
// go/keep-sorted end

$_md-sys-motion: tokens.md-sys-motion-values();
$_md-sys-shape: tokens.md-sys-shape-values();

@mixin theme($tokens) {
  $supported-tokens: tokens.$md-comp-slider-supported-tokens;

  @each $token, $value in $tokens {
    @if list.index($supported-tokens, $token) == null {
      @error 'Token `#{$token}` is not a supported token.';
    }

    @if $value {
      --md-slider-#{$token}: #{$value};
    }
  }
}

@mixin styles() {
  $tokens: tokens.md-comp-slider-values();

  // The max clip is reduced by 1 full tick display which is 2x the container
  // size to account for always showing the active track on the outside
  // edge of the last tick.
  $_active-track-max-clip: calc(
    100% - var(--_with-tick-marks-container-size) * 2
  );
  // When the start fraction is !0, add clipping by the tick container size
  $_start-fraction-not-zero: min(var(--_start-fraction) * 1e9, 1);
  $_active-track-start-offset: calc(
    var(--_with-tick-marks-container-size) * $_start-fraction-not-zero
  );
  $_active-track-start-clip: calc(
    $_active-track-start-offset + $_active-track-max-clip *
      var(--_start-fraction)
  );

  // When the end fraction is !1, add clipping by the tick container size
  $_end-fraction-not-one: min((1 - var(--_end-fraction)) * 1e9, 1);
  $_active-track-end-offset: calc(
    var(--_with-tick-marks-container-size) * $_end-fraction-not-one
  );
  $_active-track-end-clip: calc(
    $_active-track-end-offset + $_active-track-max-clip *
      (1 - var(--_end-fraction))
  );

  :host {
    @each $token, $value in $tokens {
      --_#{$token}: #{$value};
    }

    // Set these to avoid token test failures
    --_start-fraction: 0;
    --_end-fraction: 0;
    --_tick-count: 0;

    display: inline-flex;
    vertical-align: middle;
    min-inline-size: 200px;

    @include elevation.theme(
      (
        level: var(--_handle-elevation),
        shadow-color: var(--_handle-shadow-color),
      )
    );
  }

  md-focus-ring {
    height: 48px;
    inset: unset;
    width: 48px;
  }

  md-elevation {
    transition-duration: map.get($_md-sys-motion, 'duration-medium1');
    transition-timing-function: map.get($_md-sys-motion, 'emphasized-easing');
  }

  @media (prefers-reduced-motion) {
    .label {
      transition-duration: 0;
    }
  }

  // Note, opacity for active track and handle controlled via host.
  // This avoids bleed through from the handle to the track since they overlap.
  // It also means the inactive track opacity is calc'd to compensate.
  :host([disabled]) {
    opacity: var(--_disabled-active-track-opacity);

    @include elevation.theme(
      (
        level: var(--_disabled-handle-elevation),
      )
    );
  }

  .container {
    flex: 1;
    display: flex;
    align-items: center;
    position: relative;
    block-size: var(--_state-layer-size);
    // note, only the native inputs are interactive.
    pointer-events: none;
    // ensure scrolling is prevented on mobile.
    touch-action: none;
    user-select: none;
  }

  .track,
  .tickmarks {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
  }

  // inactive-track
  .track::before,
  .tickmarks::before,
  // active-track
  .track::after,
  .tickmarks::after {
    position: absolute;
    content: '';
    // pad the track inward by half the ripple size offset by the tick container size.
    $_track-padding: calc(
      (var(--_state-layer-size) / 2) - var(--_with-tick-marks-container-size)
    );
    inset-inline-start: $_track-padding;
    inset-inline-end: $_track-padding;

    // ticks size: set here since it does not change.
    background-size: calc(
        (100% - var(--_with-tick-marks-container-size) * 2) / var(--_tick-count)
      )
      100%;
  }

  // inactive-track
  .track::before,
  .tickmarks::before {
    block-size: var(--_inactive-track-height);
    border-radius: var(--_inactive-track-shape);
  }

  .track::before {
    background: var(--_inactive-track-color);
  }

  .tickmarks::before {
    background-image: _get-tick-image(
      var(--_with-tick-marks-inactive-container-color)
    );
  }

  :host([disabled]) .track::before {
    // Note, the active track opacity is applied to the entire host,
    // so the inactive track is calc'd to compensate.
    opacity: calc(
      (1 / var(--_disabled-active-track-opacity)) *
        var(--_disabled-inactive-track-opacity)
    );
    background: var(--_disabled-inactive-track-color);
  }

  // active-track
  .track::after,
  .tickmarks::after {
    block-size: var(--_active-track-height);
    border-radius: var(--_active-track-shape);
    clip-path: inset(0 $_active-track-end-clip 0 $_active-track-start-clip);
  }

  .track::after {
    background: var(--_active-track-color);
  }

  .tickmarks::after {
    background-image: _get-tick-image(
      var(--_with-tick-marks-active-container-color)
    );
  }

  // rtl for active track clipping
  .track:dir(rtl)::after {
    clip-path: inset(0 $_active-track-start-clip 0 $_active-track-end-clip);
  }

  .tickmarks:dir(rtl)::after {
    clip-path: inset(0 $_active-track-start-clip 0 $_active-track-end-clip);
  }

  :host([disabled]) .track::after {
    background: var(--_disabled-active-track-color);
  }

  :host([disabled]) .tickmarks::before {
    background-image: _get-tick-image(
      var(--_with-tick-marks-disabled-container-color)
    );
  }

  // container for the handle that is inset with padding to be
  // track-sized so that the handle container can be positioned with % only
  // and avoid a Safari issue with not being able to transition values that
  // are calced from different units.
  // TODO remove when https://bugs.webkit.org/show_bug.cgi?id=23775 is
  // addressed.
  .handleContainerPadded {
    position: relative;
    block-size: 100%;
    inline-size: 100%;
    padding-inline: calc(var(--_state-layer-size) / 2);
  }

  .handleContainerBlock {
    position: relative;
    block-size: 100%;
    inline-size: 100%;
  }

  .handleContainer {
    position: absolute;
    inset-block-start: 0;
    inset-block-end: 0;
    inset-inline-start: calc(100% * var(--_start-fraction));
    inline-size: calc(100% * (var(--_end-fraction) - var(--_start-fraction)));
  }

  // handle
  .handle {
    position: absolute;
    block-size: var(--_state-layer-size);
    inline-size: var(--_state-layer-size);
    border-radius: var(--_handle-shape);
    display: flex;
    place-content: center;
    place-items: center;
  }

  .handleNub {
    position: absolute;
    height: var(--_handle-height);
    width: var(--_handle-width);
    border-radius: var(--_handle-shape);
    background: var(--_handle-color);
  }

  :host([disabled]) .handleNub {
    background: var(--_disabled-handle-color);
  }

  input.end:focus ~ .handleContainerPadded .handle.end > .handleNub,
  input.start:focus ~ .handleContainerPadded .handle.start > .handleNub {
    background: var(--_focus-handle-color);
  }

  // prefix classes exist to overcome specificity of focus styling.
  .container > .handleContainerPadded .handle.hover > .handleNub {
    background: var(--_hover-handle-color);
  }

  :host(:not([disabled])) {
    input.end:active ~ .handleContainerPadded .handle.end > .handleNub,
    input.start:active ~ .handleContainerPadded .handle.start > .handleNub {
      background: var(--_pressed-handle-color);
    }
  }

  .onTop.isOverlapping {
    .label,
    .label::before {
      outline: var(--_with-overlap-handle-outline-color) solid
        var(--_with-overlap-handle-outline-width);
    }

    .handleNub {
      border: var(--_with-overlap-handle-outline-color) solid
        var(--_with-overlap-handle-outline-width);
    }
  }

  .handle.start {
    inset-inline-start: calc(0px - var(--_state-layer-size) / 2);
  }
  .handle.end {
    inset-inline-end: calc(0px - var(--_state-layer-size) / 2);
  }

  // label
  .label {
    position: absolute;
    box-sizing: border-box;
    display: flex;
    padding: 4px;
    place-content: center;
    place-items: center;
    border-radius: map.get($_md-sys-shape, 'corner-full');

    color: var(--_label-text-color);
    font-family: var(--_label-text-font);
    font-size: var(--_label-text-size);
    line-height: var(--_label-text-line-height);
    font-weight: var(--_label-text-weight);

    inset-block-end: 100%;
    min-inline-size: var(--_label-container-height);
    min-block-size: var(--_label-container-height);
    background: var(--_label-container-color);
    transition: transform map.get($_md-sys-motion, 'duration-short2')
      map.get($_md-sys-motion, 'easing-emphasized');
    transform-origin: center bottom;
    transform: scale(0);
  }

  // note, `:has` needed only for Safari; it's wrapped in a "forgiving"
  // `:where` since the syntax isn't supported yet in Firefox.
  :host(:focus-within) .label,
  .handleContainer.hover .label,
  :where(:has(input:active)) .label {
    transform: scale(1);
  }

  .label::before,
  .label::after {
    position: absolute;
    display: block;
    content: '';
    background: inherit;
  }

  // triangle below label
  .label::before {
    // Note, sizing carefully estimated to create an ice cream cone shape.
    $_triangleSize: calc(var(--_label-container-height) / 2);
    inline-size: $_triangleSize;
    block-size: $_triangleSize;
    bottom: calc(var(--_label-container-height) / -10);
    transform: rotate(45deg);
  }

  // fits inside label and occludes top half of triangle.
  .label::after {
    inset: 0px;
    border-radius: inherit;
  }

  // must stack above the label's pseudo-elements.
  .labelContent {
    z-index: 1;
  }

  // native input styling
  // note, the input is what the user interacts with so it must render and
  // be clickable, but it is visually hidden via opacity: 0 and non-clickable
  // styled ui is shown instead and positioned accordingly.
  input[type='range'] {
    opacity: 0;
    -webkit-tap-highlight-color: transparent;
    position: absolute;
    box-sizing: border-box;
    // needed for firefox
    height: 100%;
    width: 100%;
    margin: 0;
    background: transparent;
    cursor: pointer;
    pointer-events: auto;
    appearance: none;
  }

  input[type='range']:focus {
    outline: none;
  }

  ::-webkit-slider-runnable-track {
    -webkit-appearance: none;
  }

  ::-moz-range-track {
    appearance: none;
  }

  ::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    // note, this is sized to align with thumb
    block-size: var(--_handle-height);
    inline-size: var(--_handle-width);
    opacity: 0;
    z-index: 2;
  }

  @include _get-safari-knob-translate('end');
  @include _get-safari-knob-translate('start');

  ::-moz-range-thumb {
    appearance: none;
    block-size: var(--_state-layer-size);
    inline-size: var(--_state-layer-size);
    transform: scaleX(0);
    opacity: 0;
    z-index: 2;
  }

  // Clip the inputs to the space left/right of the center point between the
  // values so the right input gets pointer events.
  $_clip-to-start: calc(
    var(--_state-layer-size) / 2 + (100% - var(--_state-layer-size)) *
      (
        var(--_start-fraction) +
          ((var(--_end-fraction) - var(--_start-fraction)) / 2)
      )
  );

  $_clip-to-end: calc(100% - $_clip-to-start);

  // clip left side of "start" input
  .ranged input.start {
    clip-path: inset(0 $_clip-to-end 0 0);
  }

  // in 'rtl', clip right side of "lesser" input
  .ranged input.start:dir(rtl) {
    clip-path: inset(0 0 0 $_clip-to-end);
  }

  // clip right side of "end" input
  .ranged input.end {
    clip-path: inset(0 0 0 $_clip-to-start);
  }

  // in 'rtl', clip left side of "greater" input
  .ranged input.end:dir(rtl) {
    clip-path: inset(0 $_clip-to-start 0 0);
  }

  .onTop {
    z-index: 1;
  }

  // Ripple
  .handle {
    @include ripple.theme(
      (
        hover-color: var(--_hover-state-layer-color),
        hover-opacity: var(--_hover-state-layer-opacity),
        pressed-color: var(--_pressed-state-layer-color),
        pressed-opacity: var(--_pressed-state-layer-opacity),
      )
    );
  }

  md-ripple {
    border-radius: 50%;
    height: var(--_state-layer-size);
    width: var(--_state-layer-size);
  }
}

// Returns a background-image with sized circular ticks of the given color.
@function _get-tick-image($color) {
  @return radial-gradient(
    circle at var(--_with-tick-marks-container-size) center,
    #{$color} 0,
    #{$color} calc(var(--_with-tick-marks-container-size) / 2),
    transparent calc(var(--_with-tick-marks-container-size) / 2)
  );
}

// Webkit on iOS requires _some_ size on the thumb. We want to make this the
// same as --_handle-size but also be centered on the handle.
//
// the layout is similar to this:
// [()---[()===========]----]
//
// where - is the native input and the == is the material track.
// at 0 we want to shift the native knob right (padding + knob-size / 2):
// [----[(())===========]----]
//
// at the end we want to shift the native knob left by the same amount:
// [----[===========(())]----]
//
// Therefore we can do `layout-shift - 2 * `percent-fraction` * `layout-shift`
// and in RTL we want to do the the same * -1
@mixin _get-safari-knob-translate($start-or-end) {
  input.#{$start-or-end}::-webkit-slider-thumb {
    // AKA `layout-shift` in the equations above
    --_track-and-knob-padding: calc(
      (var(--_state-layer-size) - var(--_handle-width)) / 2
    );
    --_x-translate: calc(
      var(--_track-and-knob-padding) - 2 * var(--_#{$start-or-end}-fraction) *
        var(--_track-and-knob-padding)
    );
    transform: translateX(var(--_x-translate));
  }

  input.#{$start-or-end}:dir(rtl)::-webkit-slider-thumb {
    transform: translateX(calc(-1 * var(--_x-translate)));
  }
}
